package com.gitee.l0km.xthrift.base.metadata;
import com.facebook.swift.codec.metadata.ReflectionHelper;
import com.facebook.swift.codec.metadata.ThriftEnumMetadata;
import com.facebook.swift.codec.metadata.ThriftEnumMetadataBuilder;
import com.facebook.swift.codec.metadata.ThriftStructMetadata;
import com.facebook.swift.codec.metadata.ThriftType;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;

import static com.facebook.swift.codec.metadata.ReflectionHelper.getIterableType;
import static com.gitee.l0km.xthrift.base.metadata.ErpcProtocolType.ENUM;
import static com.gitee.l0km.xthrift.base.metadata.ErpcProtocolType.STRUCT;
import static com.gitee.l0km.xthrift.base.metadata.ThriftCatalogWithTransformer.CATALOG;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class ErpcType implements Comparable<ErpcType>{
    public static final ErpcType BOOL = new ErpcType(ErpcProtocolType.BOOL, boolean.class);
    public static final ErpcType INT8 = new ErpcType(ErpcProtocolType.INT8, byte.class);
    public static final ErpcType INT16 = new ErpcType(ErpcProtocolType.INT16, short.class);
    public static final ErpcType INT32 = new ErpcType(ErpcProtocolType.INT32, int.class);
    public static final ErpcType INT64 = new ErpcType(ErpcProtocolType.INT64, long.class);
    public static final ErpcType UINT8 = new ErpcType(ErpcProtocolType.UINT8, byte.class);
    public static final ErpcType UINT16 = new ErpcType(ErpcProtocolType.UINT16, short.class);
    public static final ErpcType UINT32 = new ErpcType(ErpcProtocolType.UINT32, int.class);
    public static final ErpcType UINT64 = new ErpcType(ErpcProtocolType.UINT64, long.class);
    public static final ErpcType FLOAT = new ErpcType(ErpcProtocolType.FLOAT, float.class);
    public static final ErpcType DOUBLE = new ErpcType(ErpcProtocolType.DOUBLE, double.class);
    public static final ErpcType STRING = new ErpcType(ErpcProtocolType.STRING, String.class);
    public static final ErpcType BINARY = new ErpcType(ErpcProtocolType.BINARY, byte[].class);
    public static final ErpcType BINARYB = new ErpcType(ErpcProtocolType.BINARY, ByteBuffer.class);
    public static final ErpcType VOID = new ErpcType(ErpcProtocolType.STRUCT, void.class);

    public static final ErpcType BOOLO = new ErpcType(ErpcProtocolType.BOOL, Boolean.class);
    public static final ErpcType INT8O = new ErpcType(ErpcProtocolType.INT8, Byte.class);
    public static final ErpcType INT16O = new ErpcType(ErpcProtocolType.INT16, Short.class);
    public static final ErpcType INT32O = new ErpcType(ErpcProtocolType.INT32, Integer.class);
    public static final ErpcType INT64O = new ErpcType(ErpcProtocolType.INT64, Long.class);
    public static final ErpcType UINT8O = new ErpcType(ErpcProtocolType.UINT8, Byte.class);
    public static final ErpcType UINT16O = new ErpcType(ErpcProtocolType.UINT16, Short.class);
    public static final ErpcType UINT32O = new ErpcType(ErpcProtocolType.UINT32, Integer.class);
    public static final ErpcType UINT64O = new ErpcType(ErpcProtocolType.UINT64, Long.class);
    public static final ErpcType FLOATO = new ErpcType(ErpcProtocolType.FLOAT, Float.class);
    public static final ErpcType DOUBLEO = new ErpcType(ErpcProtocolType.DOUBLE, Double.class);
    public static final ErpcType VOIDO = new ErpcType(ErpcProtocolType.STRUCT, Void.class);

    static final ImmutableMultimap<ErpcProtocolType, ErpcType> SIMPLE_TYPES = ImmutableMultimap.<ErpcProtocolType, ErpcType>builder()
    	    .put(ErpcProtocolType.BOOL,ErpcType.BOOL)
    	    .put(ErpcProtocolType.BOOL,ErpcType.BOOLO)
    	    .put(ErpcProtocolType.INT8,ErpcType.INT8)
    	    .put(ErpcProtocolType.INT8,ErpcType.INT8O)
    	    .put(ErpcProtocolType.INT16,ErpcType.INT16)
    	    .put(ErpcProtocolType.INT16,ErpcType.INT16O)
    	    .put(ErpcProtocolType.INT32,ErpcType.INT32)
    	    .put(ErpcProtocolType.INT32,ErpcType.INT32O)
    	    .put(ErpcProtocolType.INT64,ErpcType.INT64)
    	    .put(ErpcProtocolType.INT64,ErpcType.INT64O)
    	    .put(ErpcProtocolType.UINT8,ErpcType.UINT8)
    	    .put(ErpcProtocolType.UINT16,ErpcType.UINT16)
    	    .put(ErpcProtocolType.UINT16,ErpcType.UINT16O)
    	    .put(ErpcProtocolType.UINT32,ErpcType.UINT32)
    	    .put(ErpcProtocolType.UINT32,ErpcType.UINT32O)
    	    .put(ErpcProtocolType.UINT64,ErpcType.UINT64)
    	    .put(ErpcProtocolType.UINT64,ErpcType.UINT64O)
    	    .put(ErpcProtocolType.FLOAT,ErpcType.FLOAT)
    	    .put(ErpcProtocolType.FLOAT,ErpcType.FLOATO)
    	    .put(ErpcProtocolType.DOUBLE,ErpcType.DOUBLE)
    	    .put(ErpcProtocolType.DOUBLE,ErpcType.DOUBLEO)
    	    .put(ErpcProtocolType.STRING,ErpcType.STRING)
    	    .put(ErpcProtocolType.BINARY,ErpcType.BINARY)
    	    .put(ErpcProtocolType.BINARY,ErpcType.BINARYB)
    	    .build();
    private static final Map<Class<?>, ErpcType> JAVATYPES = ImmutableMap.<Class<?>, ErpcType>builder()
    		.put(boolean.class, BOOL)
    		.put(byte.class, INT8)
    		.put(short.class, INT16)
    		.put(int.class, INT32)
    		.put(long.class, INT64)
    		.put(float.class, FLOAT)
    		.put(double.class, DOUBLE)
    		.put(Boolean.class, BOOLO)
    		.put(Byte.class, INT8O)
    		.put(Short.class, INT16O)
    		.put(Integer.class, INT32O)
    		.put(Long.class, INT64O)
    		.put(Float.class, FLOATO)
    		.put(Double.class, DOUBLEO)
    		.put(byte[].class, BINARY)
    		.put(ByteBuffer.class, BINARYB)
    		.put(String.class,STRING)
    		.put(Void.class,VOIDO)
    		.put(void.class,VOID)
    		.build();
    public static ErpcType struct(ThriftStructMetadata structMetadata)
    {
        return new ErpcType(structMetadata);
    }

    public static <E> ErpcType list(ErpcType valueType)
    {
        checkNotNull(valueType, "valueType is null");
        @SuppressWarnings({ "serial", "unchecked" })
        Type javaType = new TypeToken<List<E>>(){}
                .where(new TypeParameter<E>(){}, (TypeToken<E>) TypeToken.of(valueType.getJavaType()))
                .getType();
        return new ErpcType(ErpcProtocolType.LIST, javaType, valueType);
    }

    public static ErpcType array(ErpcType valueType)
    {
        checkNotNull(valueType, "valueType is null");
        Class<?> javaType = ReflectionHelper.getArrayOfType(valueType.getJavaType());
        return new ErpcType(ErpcProtocolType.LIST, javaType, valueType);
    }

    public static ErpcType enumType(ThriftEnumMetadata<?> enumMetadata)
    {
        checkNotNull(enumMetadata, "enumMetadata is null");
        return new ErpcType(enumMetadata);
    }

    private final ErpcProtocolType protocolType;
    private final Type javaType;
    private final ErpcType valueType;
    private final ThriftStructMetadata structMetadata;
    private final ThriftEnumMetadata<?> enumMetadata;
    private ErpcType(ErpcProtocolType protocolType, Type javaType)
    {
        Preconditions.checkNotNull(protocolType, "protocolType is null");

        this.protocolType = protocolType;
        this.javaType = javaType;
        valueType = null;
        structMetadata = null;
        enumMetadata = null;
    }
    private ErpcType(ErpcProtocolType protocolType, Type javaType, ErpcType valueType)
    {
        Preconditions.checkNotNull(protocolType, "protocolType is null");
        Preconditions.checkNotNull(valueType, "valueType is null");

        this.protocolType = protocolType;
        this.javaType = javaType;
        this.valueType = valueType;
        this.structMetadata = null;
        this.enumMetadata = null;
    }

    private ErpcType(ThriftStructMetadata structMetadata)
    {
        Preconditions.checkNotNull(structMetadata, "structMetadata is null");

        this.protocolType = STRUCT;
        this.javaType = structMetadata.getStructClass();
        this.valueType = null;
        this.structMetadata = DecoratorThriftStructMetadata.STRUCT_TRANSFORMER.apply(structMetadata);
        this.enumMetadata = null;
    }

    private ErpcType(ThriftEnumMetadata<?> enumMetadata)
    {
        Preconditions.checkNotNull(enumMetadata, "enumMetadata is null");

        this.protocolType = ENUM;
        this.javaType = enumMetadata.getEnumClass();
        this.valueType = null;
        this.structMetadata = null;
        this.enumMetadata = DecoratorThriftEnumMetadata.ENUM_TRANSFORMER.apply(enumMetadata);
    }

    public ErpcProtocolType getProtocolType()
    {
        return protocolType;
    }
	public Type getJavaType() {
		return javaType;
	}
	public Class<?> getJavaClass() {
		checkNotNull(javaType,"javaType is null");
		return TypeToken.of(javaType).getRawType();
	}
	public Class<?> getUnwrappedJavaType() {
		checkNotNull(javaType,"javaType is null");
		if(javaType instanceof Class<?>){
			return Primitives.unwrap((Class<?>) javaType);
		}		
		return TypeToken.of(javaType).getRawType();
	}
	public ErpcType getValueType()
    {
        checkState(valueType != null, "%s does not have a value", protocolType);
        return valueType;
    }

    public ThriftStructMetadata getStructMetadata()
    {
        checkState(structMetadata != null, "%s does not have struct metadata", protocolType);
        return structMetadata;
    }

    public ThriftEnumMetadata<?> getEnumMetadata()
    {
        checkState(enumMetadata != null, "%s does not have enum metadata", protocolType);
        return enumMetadata;
    }

    public static ErpcType fromJavaType(Type type){
    	ErpcType res = JAVATYPES.get(type);
    	if(res == null){
    		if(type instanceof Class<?>){
    			for(Entry<Class<?>, ErpcType> entry : JAVATYPES.entrySet()){
    				if(entry.getKey().isAssignableFrom((Class<?>) type)){
    					res = entry.getValue();
    					break;
    				}
    			}
    		}
    		if(res == null){
    			res = getErpcType(CATALOG.getThriftType(type));
    		}
    	}
    	return res;
    }
    
    public boolean isVoid(){
    	return this.equals(VOID) || this.equals(VOIDO);
    }

    public String toTypeString()
	{
		if(getProtocolType().simpleType){
			return getProtocolType().name().toLowerCase();
		}
		switch (getProtocolType()) {
	        case ENUM:      return  getEnumMetadata().getEnumName();
	        case LIST:      return "list<" + getValueType().toTypeString() + ">";
	        // void is encoded as a struct
	        case STRUCT:    return this == ErpcType.VOID  ? "void" : getStructMetadata().getStructName();
	        default:
	        	break;
	    }
	
	    throw new IllegalStateException("Bad protocol type" + getProtocolType());
	}
    
    private String compontentType(){
		ErpcType vt = getValueType();
		if(vt.protocolType.simpleType){
			return vt.getProtocolType().name().toLowerCase();
		}
		switch(vt.protocolType){
		case STRING:
			return "string";
		case BINARY:
			return "binary";
		default:
			return vt.getCtype();
		}
    }
    public String getCtype(){
    	switch (getProtocolType()) {
		case FLOAT:
		case DOUBLE:
    	case BOOL:
			return getProtocolType().name().toLowerCase();
		case STRUCT:
		case UNION:
		case ENUM:
			return TypeToken.of(javaType).getRawType().getSimpleName() + "_t";
		case LIST:			
			return String.format("list_%s_1_t",compontentType());
		case STRING:
			return "char *";
		case BINARY:
		case INT8:
		case INT16:
		case INT32:
		case INT64:
		case UINT8:
		case UINT16:
		case UINT32:
		case UINT64:
		default:
			return getProtocolType().name().toLowerCase() + "_t";
		}
    }
    public String getCtype(boolean withConstant,boolean byRef){
    	String constPrefix = withConstant ? "const " : "";
    	String pointerSuffix = byRef && !getProtocolType().equals(ErpcProtocolType.STRING) ? " *" : "";
    	return constPrefix + getCtype() + pointerSuffix;
    }
    public String toPreAllocTypeString(int size){
    	switch (getProtocolType()) {
    	case BINARY:
    		return "uint8[" + size + "]";
    	case STRING:
    		return "int8[" + (size + 1) + "]";
    	case LIST:
    		return getValueType().toTypeString() + "[" + size  + "]";
    	default:
    		return toTypeString();
    	}	
    }
    
    public String getPreAllocTypeFormat(int size){
    	if(size < 0){
    		return toTypeString() + " %s";
    	}
    	switch (getProtocolType()) {
    	case BINARY:
    		return "uint8 %s[" + size + "]";
    	case STRING:
    		return "int8 %s[" + (size + 1) + "]";
    	case LIST:
    		return getValueType().toTypeString() + "  %s[" + size  + "]";
    	default:
    		return toTypeString() + " %s";
    	}	
    }
	@Override
    public boolean equals(Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        final ErpcType that = (ErpcType) o;
        if (javaType != null ? !javaType.equals(that.javaType) : that.javaType != null) {
            return false;
        }
        if (protocolType != that.protocolType) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode()
    {
        int result = protocolType != null ? protocolType.hashCode() : 0;
        result = 31 * result + (javaType != null ? javaType.hashCode() : 0);
        return result;
    }

    @Override
    public String toString()
    {
        final StringBuilder sb = new StringBuilder();
        sb.append("ErpcType");
        sb.append("{");
        sb.append(protocolType).append(" ").append(javaType);
        if (structMetadata != null) {
            sb.append(" ").append(structMetadata.getStructClass().getName());
        }
        else if (valueType != null) {
            sb.append(" valueType=").append(valueType);
        }
        sb.append('}');
        return sb.toString();
    }

	public static ErpcType getErpcType(ThriftType thriftType){
		ErpcProtocolType erpcProtocolType = ErpcProtocolType.erpcTypeOf(thriftType.getProtocolType());
		ImmutableCollection<ErpcType> simpleType = SIMPLE_TYPES.get(erpcProtocolType);
		if(!simpleType.isEmpty()){
			for(ErpcType erpcType:simpleType){
				if(erpcType.javaType.equals(thriftType.getJavaType())){
					return erpcType;
				}
			}
			throw new IllegalStateException("NOT FOUND ErpcType instance for " + thriftType.toString());
		}
		Class<?> rawType = TypeToken.of(thriftType.getJavaType()).getRawType();
		switch(erpcProtocolType){
		case ENUM:
			@SuppressWarnings({ "rawtypes", "unchecked" }) 
			ThriftEnumMetadata<? extends Enum> enumMetadata 
				= new ThriftEnumMetadataBuilder<>((Class<? extends Enum>) rawType).build();
			return enumType(enumMetadata);
		case LIST:
			if (Iterable.class.isAssignableFrom(rawType)) {
				Type elementType = getIterableType(thriftType.getJavaType());
				return list(fromJavaType(elementType));	
			}
		case STRUCT:
			if(ThriftType.VOID.equals(thriftType)){
				return VOID;
			}
            // An union looks like a struct with a single field.
            return struct(thriftType.getStructMetadata());				
		case UNION:
			 // An union looks like a struct with a single field.
	        return struct(thriftType.getStructMetadata());
		default:
			throw new IllegalStateException("UNSUPPORTED ERPC TYPE " + erpcProtocolType );
		}
	}

	@Override
	public int compareTo(ErpcType o) {
		return getCtype().compareTo(o.getCtype());	
	}
}
